Repository: burnash/gspread

Issue:
A way to add DataValidation
**Is your feature request related to a problem? Please describe.**
The API allows to setup data validation on a range via the `batchUpdate` end point: https://developers.google.com/sheets/api/samples/data#data-validation

**Describe the solution you'd like**
Since it is part of the `batchUpdate` endpoint, anyone can use the low-level method to pass through the data. However, there should be a high-level method. or a kwarg in the low-level [`values_update`](https://github.com/burnash/gspread/blob/4a48fbdc4bc56724d3a1d8fcdb7f5793fb58f160/gspread/http_client.py#L138) method.  We should create a class `DataValidation` class, which will have all the options provided by the API.

It would have `to_json` method, that will be passed to the update payload when sending the request.

**Describe alternatives you've considered**
Passing a `kwarg` to the update methods to pass through json data.


Discussion:
alifeee: hi :) thanks for the PR

it looks like you have to run `tox -e format` locally to format the code, so the linter is happy.

after that, the CI should pass, and we will be able to review this change :)
alifeee: Hi, that looks good. Sorry there keep being issues.

Try deleting the cassette, and re-running the test to generate it again. Sometimes there are conflicts.
lavigne958: What type of cassette recording mode did you use ? 
lavigne958: I checked and the tests won't pass until you remove the following file and record it again.
`tests/cassettes/WorksheetTest.test_add_validation.json`

Quick trick, in order to regenerate only this cassette you can use the following command:
`tox -e py -- -k test_add_validation`
don't forget to set the env variables `GS_CREDS_FILENAME` and `GS_RECORD_MODE=new_episodes` so it only records this missing cassette.
lavigne958: Great thanks. Last thing left is the documentation to fix then all tests should pass. 
muddi900: Seems all tests are working, I'll flatten the commits in a moment
alifeee: this looks wonderful

would we like to put it in any docs like the main `rst` example file, or the README, so that people actually know this new function exists?

:)
muddi900: If there is a markdown to rst converter, I'll be game.

On Fri, Mar 29, 2024, 8:21 PM alifeee ***@***.***> wrote:

> this looks wonderful
>
> would we like to put it in any docs like the main rst example file, or
> the README, so that people actually know this new function exists?
>
> :)
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/burnash/gspread/pull/1444#issuecomment-2027377602>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AI2VVZQDII7B4N66JO726A3Y2V2FNAVCNFSM6AAAAABFAW7QIOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMRXGM3TONRQGI>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

alifeee: hmm, not sure about markdown to `rst`...

but I would add a simple code snippet, like
```python
ws.add_validation(
                'C2:C7',
                ValidationConditionType.one_of_list,
                ['Yes','No'],
                showCustomUi=True
            )
```

to the README (1)...

<https://github.com/burnash/gspread/blob/master/README.md?plain=1>

...and the user guide (2)...

<https://github.com/burnash/gspread/blob/master/docs/user-guide.rst?plain=1>

... in an appropriate place. For (1), it's markdown. For (2), you can run `tox -e doc` to build the docs to see if they build properly.

Patch:
diff --git a/README.md b/README.md
index 319eaa9..30f2c5e 100644
--- a/README.md
+++ b/README.md
@@ -292,6 +292,30 @@ from gspread.utils import ValueRenderOption
 # Get formula from a cell
 >>> worksheet.get("C2:D2", value_render_option=ValueRenderOption.formula)
 [['=1/1024']]
+### Add data validation to a range
+
+```python
+import gspread
+from gspread.utils import ValidationConditionType
+
+# Restrict the input to greater than 10 in a single cell
+worksheet.add_validation(
+  'A1',
+  ValidationConditionType.number_greater,
+  [10],
+  strict=True,
+  inputMessage='Value must be greater than 10',
+)
+
+# Restrict the input to Yes/No for a specific range with dropdown
+worksheet.add_validation(
+  'C2:C7',
+   ValidationConditionType.one_of_list,
+   ['Yes',
+   'No',]
+   showCustomUi=True
+)
+
 ```
 
 ## Documentation
diff --git a/docs/user-guide.rst b/docs/user-guide.rst
index b3806b5..70712ac 100644
--- a/docs/user-guide.rst
+++ b/docs/user-guide.rst
@@ -316,6 +316,44 @@ Update a range
 
    worksheet.update('A1:B2', [[1, 2], [3, 4]])
 
+
+Adding Data Validation
+~~~~~~~~~~~~~~~~~~~~~~
+
+You can add a strict validation to a cell.
+
+.. code:: python
+
+   ws.add_validation(
+      'A1',
+      ValidationConditionType.number_greater,
+      [10],
+      strict=True,
+      inputMessage='Value must be greater than 10',
+   )
+ 
+
+Or add validation with a drop down.
+
+.. code:: python
+   
+   worksheet.add_validation(
+      'C2:C7',
+      ValidationConditionType.one_of_list,
+      ['Yes',
+      'No',]
+      showCustomUi=True
+   )
+
+
+Check out the api docs for `DataValidationRule`_ and `CondtionType`_ for more details.
+
+.. _CondtionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType
+
+.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule 
+
+
+
 Formatting
 ~~~~~~~~~~
 
diff --git a/gspread/utils.py b/gspread/utils.py
index 59d2581..ff5c255 100644
--- a/gspread/utils.py
+++ b/gspread/utils.py
@@ -118,6 +118,41 @@ class GridRangeType(StrEnum):
     ListOfLists = "ListOfLists"
 
 
+class ValidationConditionType(StrEnum):
+    number_greater = "NUMBER_GREATER"
+    number_greater_than_eq = "NUMBER_GREATER_THAN_EQ"
+    number_less = "NUMBER_LESS"
+    number_less_than_eq = "NUMBER_LESS_THAN_EQ"
+    number_eq = "NUMBER_EQ"
+    number_not_eq = "NUMBER_NOT_EQ"
+    number_between = "NUMBER_BETWEEN"
+    number_not_between = "NUMBER_NOT_BETWEEN"
+    text_contains = "TEXT_CONTAINS"
+    text_not_contains = "TEXT_NOT_CONTAINS"
+    text_starts_with = "TEXT_STARTS_WITH"
+    text_ends_with = "TEXT_ENDS_WITH"
+    text_eq = "TEXT_EQ"
+    text_is_email = "TEXT_IS_EMAIL"
+    text_is_url = "TEXT_IS_URL"
+    date_eq = "DATE_EQ"
+    date_before = "DATE_BEFORE"
+    date_after = "DATE_AFTER"
+    date_on_or_before = "DATE_ON_OR_BEFORE"
+    date_on_or_after = "DATE_ON_OR_AFTER"
+    date_between = "DATE_BETWEEN"
+    date_not_between = "DATE_NOT_BETWEEN"
+    date_is_valid = "DATE_IS_VALID"
+    one_of_range = "ONE_OF_RANGE"
+    one_of_list = "ONE_OF_LIST"
+    blank = "BLANK"
+    not_blank = "NOT_BLANK"
+    custom_formula = "CUSTOM_FORMULA"
+    boolean = "BOOLEAN"
+    text_not_eq = "TEXT_NOT_EQ"
+    date_not_eq = "DATE_NOT_EQ"
+    filter_expression = "FILTER_EXPRESSION"
+
+
 def convert_credentials(credentials: Credentials) -> Credentials:
     module = credentials.__module__
     cls = credentials.__class__.__name__
diff --git a/gspread/worksheet.py b/gspread/worksheet.py
index 5710a4f..09f6849 100644
--- a/gspread/worksheet.py
+++ b/gspread/worksheet.py
@@ -41,6 +41,7 @@ from .utils import (
     PasteOrientation,
     PasteType,
     T,
+    ValidationConditionType,
     ValueInputOption,
     ValueRenderOption,
     a1_range_to_grid_range,
@@ -2091,12 +2092,14 @@ class Worksheet:
                             "description": description,
                             "warningOnly": warning_only,
                             "requestingUserCanEdit": requesting_user_can_edit,
-                            "editors": None
-                            if warning_only
-                            else {
-                                "users": editor_users_emails,
-                                "groups": editor_groups_emails,
-                            },
+                            "editors": (
+                                None
+                                if warning_only
+                                else {
+                                    "users": editor_users_emails,
+                                    "groups": editor_groups_emails,
+                                }
+                            ),
                         }
                     }
                 }
@@ -3201,3 +3204,87 @@ class Worksheet:
         }
 
         return self.client.batch_update(self.spreadsheet_id, body)
+
+    def add_validation(
+        self,
+        range: str,
+        condition_type: ValidationConditionType,
+        values: Iterable[Any],
+        inputMessage: Optional[str] = None,
+        strict: bool = False,
+        showCustomUi: bool = False,
+    ) -> Any:
+        """Adds a data validation rule to any given range.
+
+        .. note::
+
+            ``condition_type`` values are explained here: `ConditionType`_
+
+            .. _ConditionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType
+
+
+        :param str source: The A1 notation of the source range to move
+        :param condition_type: The sort of condition to apply.
+        :param values: List of condition values.
+        :type values: Any
+        :param str inputMessage: Message to show for the validation.
+        :param bool strict: Whether to reject invalid data or not.
+        :param bool showCustomUi: Whether to show a custom UI(Dropdown) for list values.
+
+        **Examples**
+
+        .. code-block:: python
+
+            import gspread
+            from gspread.utils import ValidationConditionType
+
+
+            ...
+
+            ws = spreadsheet.sheet1
+
+            ws.add_validation(
+                'A1',
+                ValidationConditionType.number_greater,
+                [10],
+                strict=True,
+                inputMessage='Value must be greater than 10',
+            )
+
+            ws.add_validation(
+                'C2:C7',
+                ValidationConditionType.one_of_list,
+                ['Yes','No'],
+                showCustomUi=True
+            )
+        """
+
+        if not isinstance(condition_type, ValidationConditionType):
+            raise TypeError(
+                "condition_type param should be a valid ValidationConditionType."
+            )
+
+        grid = a1_range_to_grid_range(range, self.id)
+
+        body = {
+            "requests": [
+                {
+                    "setDataValidation": {
+                        "range": grid,
+                        "rule": {
+                            "condition": {
+                                "type": condition_type,
+                                "values": [
+                                    ({"userEnteredValue": value}) for value in values
+                                ],
+                            },
+                            "showCustomUi": showCustomUi,
+                            "strict": strict,
+                            "inputMessage": inputMessage,
+                        },
+                    }
+                }
+            ],
+        }
+
+        return self.client.batch_update(self.spreadsheet_id, body)


Test patch:
diff --git a/tests/cassettes/WorksheetTest.test_add_validation.json b/tests/cassettes/WorksheetTest.test_add_validation.json
new file mode 100644
index 0000000..a690268
--- /dev/null
+++ b/tests/cassettes/WorksheetTest.test_add_validation.json
@@ -0,0 +1,539 @@
+{
+    "version": 1,
+    "interactions": [
+        {
+            "request": {
+                "method": "POST",
+                "uri": "https://www.googleapis.com/drive/v3/files?supportsAllDrives=True",
+                "body": "{\"name\": \"Test WorksheetTest test_add_validation\", \"mimeType\": \"application/vnd.google-apps.spreadsheet\"}",
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "105"
+                    ],
+                    "Content-Type": [
+                        "application/json"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:41 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin, X-Origin"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "no-cache, no-store, max-age=0, must-revalidate"
+                    ],
+                    "Expires": [
+                        "Mon, 01 Jan 1990 00:00:00 GMT"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "Pragma": [
+                        "no-cache"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "192"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"kind\": \"drive#file\",\n  \"id\": \"1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA\",\n  \"name\": \"Test WorksheetTest test_add_validation\",\n  \"mimeType\": \"application/vnd.google-apps.spreadsheet\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "GET",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA?includeGridData=false",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:42 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "3336"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA\",\n  \"properties\": {\n    \"title\": \"Test WorksheetTest test_add_validation\",\n    \"locale\": \"en_US\",\n    \"autoRecalc\": \"ON_CHANGE\",\n    \"timeZone\": \"Etc/GMT\",\n    \"defaultFormat\": {\n      \"backgroundColor\": {\n        \"red\": 1,\n        \"green\": 1,\n        \"blue\": 1\n      },\n      \"padding\": {\n        \"top\": 2,\n        \"right\": 3,\n        \"bottom\": 2,\n        \"left\": 3\n      },\n      \"verticalAlignment\": \"BOTTOM\",\n      \"wrapStrategy\": \"OVERFLOW_CELL\",\n      \"textFormat\": {\n        \"foregroundColor\": {},\n        \"fontFamily\": \"arial,sans,sans-serif\",\n        \"fontSize\": 10,\n        \"bold\": false,\n        \"italic\": false,\n        \"strikethrough\": false,\n        \"underline\": false,\n        \"foregroundColorStyle\": {\n          \"rgbColor\": {}\n        }\n      },\n      \"backgroundColorStyle\": {\n        \"rgbColor\": {\n          \"red\": 1,\n          \"green\": 1,\n          \"blue\": 1\n        }\n      }\n    },\n    \"spreadsheetTheme\": {\n      \"primaryFontFamily\": \"Arial\",\n      \"themeColors\": [\n        {\n          \"colorType\": \"TEXT\",\n          \"color\": {\n            \"rgbColor\": {}\n          }\n        },\n        {\n          \"colorType\": \"BACKGROUND\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 1,\n              \"blue\": 1\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT1\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.25882354,\n              \"green\": 0.52156866,\n              \"blue\": 0.95686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT2\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.91764706,\n              \"green\": 0.2627451,\n              \"blue\": 0.20784314\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT3\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.9843137,\n              \"green\": 0.7372549,\n              \"blue\": 0.015686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT4\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.20392157,\n              \"green\": 0.65882355,\n              \"blue\": 0.3254902\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT5\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 0.42745098,\n              \"blue\": 0.003921569\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT6\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.27450982,\n              \"green\": 0.7411765,\n              \"blue\": 0.7764706\n            }\n          }\n        },\n        {\n          \"colorType\": \"LINK\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.06666667,\n              \"green\": 0.33333334,\n              \"blue\": 0.8\n            }\n          }\n        }\n      ]\n    }\n  },\n  \"sheets\": [\n    {\n      \"properties\": {\n        \"sheetId\": 0,\n        \"title\": \"Sheet1\",\n        \"index\": 0,\n        \"sheetType\": \"GRID\",\n        \"gridProperties\": {\n          \"rowCount\": 1000,\n          \"columnCount\": 26\n        }\n      }\n    }\n  ],\n  \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA/edit\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "GET",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA?includeGridData=false",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:42 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "3336"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA\",\n  \"properties\": {\n    \"title\": \"Test WorksheetTest test_add_validation\",\n    \"locale\": \"en_US\",\n    \"autoRecalc\": \"ON_CHANGE\",\n    \"timeZone\": \"Etc/GMT\",\n    \"defaultFormat\": {\n      \"backgroundColor\": {\n        \"red\": 1,\n        \"green\": 1,\n        \"blue\": 1\n      },\n      \"padding\": {\n        \"top\": 2,\n        \"right\": 3,\n        \"bottom\": 2,\n        \"left\": 3\n      },\n      \"verticalAlignment\": \"BOTTOM\",\n      \"wrapStrategy\": \"OVERFLOW_CELL\",\n      \"textFormat\": {\n        \"foregroundColor\": {},\n        \"fontFamily\": \"arial,sans,sans-serif\",\n        \"fontSize\": 10,\n        \"bold\": false,\n        \"italic\": false,\n        \"strikethrough\": false,\n        \"underline\": false,\n        \"foregroundColorStyle\": {\n          \"rgbColor\": {}\n        }\n      },\n      \"backgroundColorStyle\": {\n        \"rgbColor\": {\n          \"red\": 1,\n          \"green\": 1,\n          \"blue\": 1\n        }\n      }\n    },\n    \"spreadsheetTheme\": {\n      \"primaryFontFamily\": \"Arial\",\n      \"themeColors\": [\n        {\n          \"colorType\": \"TEXT\",\n          \"color\": {\n            \"rgbColor\": {}\n          }\n        },\n        {\n          \"colorType\": \"BACKGROUND\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 1,\n              \"blue\": 1\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT1\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.25882354,\n              \"green\": 0.52156866,\n              \"blue\": 0.95686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT2\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.91764706,\n              \"green\": 0.2627451,\n              \"blue\": 0.20784314\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT3\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.9843137,\n              \"green\": 0.7372549,\n              \"blue\": 0.015686275\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT4\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.20392157,\n              \"green\": 0.65882355,\n              \"blue\": 0.3254902\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT5\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 1,\n              \"green\": 0.42745098,\n              \"blue\": 0.003921569\n            }\n          }\n        },\n        {\n          \"colorType\": \"ACCENT6\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.27450982,\n              \"green\": 0.7411765,\n              \"blue\": 0.7764706\n            }\n          }\n        },\n        {\n          \"colorType\": \"LINK\",\n          \"color\": {\n            \"rgbColor\": {\n              \"red\": 0.06666667,\n              \"green\": 0.33333334,\n              \"blue\": 0.8\n            }\n          }\n        }\n      ]\n    }\n  },\n  \"sheets\": [\n    {\n      \"properties\": {\n        \"sheetId\": 0,\n        \"title\": \"Sheet1\",\n        \"index\": 0,\n        \"sheetType\": \"GRID\",\n        \"gridProperties\": {\n          \"rowCount\": 1000,\n          \"columnCount\": 26\n        }\n      }\n    }\n  ],\n  \"spreadsheetUrl\": \"https://docs.google.com/spreadsheets/d/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA/edit\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "POST",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA/values/%27Sheet1%27:clear",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "0"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:42 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "107"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA\",\n  \"clearedRange\": \"Sheet1!A1:Z1000\"\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "POST",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA:batchUpdate",
+                "body": "{\"requests\": [{\"setDataValidation\": {\"range\": {\"startRowIndex\": 0, \"endRowIndex\": 1, \"startColumnIndex\": 0, \"endColumnIndex\": 1, \"sheetId\": 0}, \"rule\": {\"condition\": {\"type\": \"ONE_OF_LIST\", \"values\": [{\"userEnteredValue\": \"y\"}]}, \"showCustomUi\": false, \"strict\": true, \"inputMessage\": \"n\"}}}]}",
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "293"
+                    ],
+                    "Content-Type": [
+                        "application/json"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 200,
+                    "message": "OK"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:43 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "97"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"spreadsheetId\": \"1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA\",\n  \"replies\": [\n    {}\n  ]\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "PUT",
+                "uri": "https://sheets.googleapis.com/v4/spreadsheets/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA/values/%27Sheet1%27%21A1?valueInputOption=RAW",
+                "body": "{\"values\": \"X\", \"majorDimension\": null}",
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "39"
+                    ],
+                    "Content-Type": [
+                        "application/json"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 400,
+                    "message": "Bad Request"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:43 GMT"
+                    ],
+                    "Transfer-Encoding": [
+                        "chunked"
+                    ],
+                    "Vary": [
+                        "Origin",
+                        "X-Origin",
+                        "Referer"
+                    ],
+                    "x-l2-request-path": [
+                        "l2-managed-6"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "private"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "application/json; charset=UTF-8"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ],
+                    "content-length": [
+                        "491"
+                    ]
+                },
+                "body": {
+                    "string": "{\n  \"error\": {\n    \"code\": 400,\n    \"message\": \"Invalid value at 'data.values' (type.googleapis.com/google.protobuf.ListValue), \\\"X\\\"\",\n    \"status\": \"INVALID_ARGUMENT\",\n    \"details\": [\n      {\n        \"@type\": \"type.googleapis.com/google.rpc.BadRequest\",\n        \"fieldViolations\": [\n          {\n            \"field\": \"data.values\",\n            \"description\": \"Invalid value at 'data.values' (type.googleapis.com/google.protobuf.ListValue), \\\"X\\\"\"\n          }\n        ]\n      }\n    ]\n  }\n}\n"
+                }
+            }
+        },
+        {
+            "request": {
+                "method": "DELETE",
+                "uri": "https://www.googleapis.com/drive/v3/files/1yOY1U4tsuCpKdLXnrrfpGtUtmIFCHLzXbYVJrbLlmCA?supportsAllDrives=True",
+                "body": null,
+                "headers": {
+                    "User-Agent": [
+                        "python-requests/2.31.0"
+                    ],
+                    "Accept-Encoding": [
+                        "gzip, deflate"
+                    ],
+                    "Accept": [
+                        "*/*"
+                    ],
+                    "Connection": [
+                        "keep-alive"
+                    ],
+                    "Content-Length": [
+                        "0"
+                    ],
+                    "authorization": [
+                        "<ACCESS_TOKEN>"
+                    ]
+                }
+            },
+            "response": {
+                "status": {
+                    "code": 204,
+                    "message": "No Content"
+                },
+                "headers": {
+                    "Date": [
+                        "Fri, 29 Mar 2024 06:00:44 GMT"
+                    ],
+                    "Vary": [
+                        "Origin, X-Origin"
+                    ],
+                    "Content-Length": [
+                        "0"
+                    ],
+                    "X-XSS-Protection": [
+                        "0"
+                    ],
+                    "Cache-Control": [
+                        "no-cache, no-store, max-age=0, must-revalidate"
+                    ],
+                    "Expires": [
+                        "Mon, 01 Jan 1990 00:00:00 GMT"
+                    ],
+                    "Alt-Svc": [
+                        "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"
+                    ],
+                    "Pragma": [
+                        "no-cache"
+                    ],
+                    "X-Frame-Options": [
+                        "SAMEORIGIN"
+                    ],
+                    "Content-Type": [
+                        "text/html"
+                    ],
+                    "X-Content-Type-Options": [
+                        "nosniff"
+                    ],
+                    "Server": [
+                        "ESF"
+                    ]
+                },
+                "body": {
+                    "string": ""
+                }
+            }
+        }
+    ]
+}
diff --git a/tests/worksheet_test.py b/tests/worksheet_test.py
index 1c2e9b3..8ee98b9 100644
--- a/tests/worksheet_test.py
+++ b/tests/worksheet_test.py
@@ -1871,3 +1871,18 @@ class WorksheetTest(GspreadTest):
 
         # get_all_values should be a carbon copy of get_values
         self.assertEqual(sig_get_values, sig_get_all_values)
+
+    @pytest.mark.vcr()
+    def test_add_validation(self):
+        sheet = self.sheet
+        self.assertDictEqual(
+            sheet.add_validation(
+                "A1",
+                utils.ValidationConditionType.one_of_list,
+                ["y", "n"],
+                strict=True,
+            ),
+            {"spreadsheetId": self.spreadsheet.id, "replies": [{}]},
+        )
+
+        self.assertRaises(APIError, sheet.update, values="X", range_name="A1")