You wrote a Python script that reads or writes a Google Sheet. It ran fine for about a week, then every run started failing with:

google.auth.exceptions.RefreshError: ('invalid_grant: Token has been expired or revoked.', ...) 

Nothing in your code changed. Here is why this happens, how to confirm it, and the fix.

Why it happens

If your script logs in with OAuth (a browser window asks you to click "Allow"), Google gives it a short-lived access token and a long-lived refresh token. gspread saves the refresh token in authorized_user.json. When the refresh token stops working, every run fails with invalid_grant until you do the browser step again.

Google's OAuth 2.0 documentation says that a project whose consent screen is External with publishing status Testing gets a refresh token that expires in 7 days. The exception is apps that request only name, e-mail and profile scopes. Spreadsheet scopes are not in that list, so the rule applies to you.

When you follow a tutorial, the consent screen starts in Testing. So everything works, and a week later it breaks.

Is it really this?

What you see Likely cause
Worked for about 7 days after you authorised, then invalid_grant Consent screen in Testing
Fails on the very first run Not expiry. Wrong client type, API not enabled, or scope mismatch
Worked for months, then invalid_grant Access revoked, token unused for six months, or too many tokens (limit: 100 per account per OAuth client)

To check: in the Cloud Console search for "OAuth consent screen" (Google is renaming it to Google Auth Platform, so also try "Audience") and look at the publishing status.

Fix 1: use a service account (best for anything unattended)

A service account is a robot Google identity. It has no password and no refresh token, so there is nothing to expire after 7 days.

  1. In your Cloud project, enable the Google Sheets API.
  2. Create a service account (IAM & Admin). You can skip the optional role steps.
  3. On the service account: Keys → Add key → Create new key → JSON. Save the file. It is a password; never commit it.
  4. Open the JSON and copy the client_email value.
  5. Share your spreadsheet with that e-mail as Editor.
import gspread gc = gspread.service_account(filename="service_account.json") ws = gc.open_by_key("YOUR_SHEET_ID").sheet1 print(ws.get("A1")) 

If you get [403] The caller does not have permission, the sheet is not shared with that e-mail, or only as Viewer. If you get Google Sheets API has not been used in project ... or it is disabled, you enabled the API in a different Cloud project than the one that owns the key.

In CI, put the whole JSON file, unmodified, into one secret. If the key loses its \n line breaks in the paste you get Could not deserialize key data.

Fix 2: stay on OAuth, but publish the consent screen

Change the publishing status from Testing to In production. The 7-day expiry stops applying to newly issued tokens (delete the old authorized_user.json and authorise once more). Read this first: because spreadsheet scopes are sensitive, an unverified app shows an "unverified app" warning and is limited to 100 new users. For a script that only you use, that is fine. In a Google Workspace organisation, an Internal app avoids both the verification and the 7-day limit.

Fix 3: accept it, but make it loud

If you keep OAuth, catch the error and say what to do, instead of a bare traceback:

import gspread from google.auth.exceptions import RefreshError try: gc = gspread.oauth() gc.open_by_key("YOUR_SHEET_ID").sheet1.get("A1") except RefreshError as exc: raise SystemExit( "Google rejected the saved login (invalid_grant). Delete authorized_user.json and run again to re-authorise. " f"Original error: {exc}" ) 

And never run the interactive browser flow from a scheduled job.

A checklist before you schedule anything

I wrote a one-page checklist for the three things that most often kill a scheduled Sheets script (this token problem, 429 Quota exceeded, and duplicate rows): https://whop.com/whop-0bab/day-8-checklist-python-google-sheets-free/. It is free.

Disclosure: I also sell a short pack that fixes exactly those three failures with tested code and a connection checker (EUR 9): https://whop.com/whop-0bab/python-google-sheets-fix-pack/. Everything in this post works without it.